﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Linq2Crm.Helper;
using Linq2Crm.ObjectModel;
using Linq2Crm.ObjectModel.Attributes;
using SilverCrmSoap.CrmSdk;


namespace Linq2Crm.Interpreter
{
	internal partial class ExpressionInterpreter
	{
		#region Members

		private Dictionary<LogicalOperator, List<ConditionExpression>> _filters = new Dictionary<LogicalOperator, List<ConditionExpression>>()
		                                                                          	{
		                                                                          		{LogicalOperator.And, new List<ConditionExpression>()},
		                                                                          		{LogicalOperator.Or, new List<ConditionExpression>()}
		                                                                          	};

		private ConditionExpression _currentCondition;

		/// <summary>
		/// </summary>
		private OrderExpression _currentOder;

		/// <summary>
		/// </summary>
		private PagingInfo _currentPaging;

		/// <summary>
		/// </summary>
		private Type _domainObjectType;

		private Type _additionObjectType;

		/// <summary>
		/// </summary>
		private QueryExpression _queryExpession;

		private int _skip;

		private int _take;


		#endregion

		#region Main Entry Methods

		/// <summary>
		/// 	Lins the Q to query expression.
		/// </summary>
		/// <param name="ex"> The LinQ Expression. </param>
		/// <returns> An ExpressionResult instance containing a CRM Query Expression. </returns>
		public ExpressionResult LinQToQueryExpression(Expression ex)
		{
			_queryExpession = new QueryExpression { ColumnSet = new ColumnSet() };
			Visit(ex);

			return new ExpressionResult(_queryExpession, _domainObjectType,_skip,_take);
		}

		#endregion

		#region LinQ2QueryExpression Helpers

		/// <summary>
		/// 	Maps the name of the attribute name to CRM schema.
		/// </summary>
		/// <param name="attributeName"> Name of the attribute. </param>
		/// <param name="domainObject"> The domain object. </param>
		/// <returns> Mapped CRM attribute name. </returns>
		protected string MapAttributeNameToCrmSchemaName(string attributeName, Type domainObject)
		{
         var entityNameAttr = domainObject.GetCustomAttributes(typeof(CrmEntityMappingAttribute), true).OfType<CrmEntityMappingAttribute>().FirstOrDefault();
			if (String.IsNullOrEmpty(attributeName))
			{
            return entityNameAttr == null
							? null
                     : entityNameAttr.EntitySchemaName;
			}
			PropertyInfo property = domainObject.GetProperty(attributeName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
			if (property != null)
			{
            string attrResultName = null;
				switch (property.Name)
				{
				   case "StateCode":
				   case "StatusCode":
				      attrResultName = property.Name.ToLower();
                  break;
               case "Id":
                  {
                     var entityAttr = domainObject.GetCustomAttributes(typeof(CrmEntityMappingAttribute), true).OfType<CrmEntityMappingAttribute>().FirstOrDefault();
                     if (entityAttr == null)
                     {
                        return null;
                     }
                     attrResultName = entityAttr.IDField;
                     break;
                  }
               case "Name":
				      {
				         var entityAttr = domainObject.GetCustomAttributes(typeof(CrmEntityMappingAttribute), true).OfType<CrmEntityMappingAttribute>().FirstOrDefault();
				         if (entityAttr == null)
				         {
				            return null;
				         }
				         attrResultName = entityAttr.NameField;
                     break;
				      }
				}

            if (String.IsNullOrEmpty(attrResultName))
            {
               attrResultName = property.GetCustomAttributes(typeof(CrmFieldMappingAttribute), false).OfType<CrmFieldMappingAttribute>().Select(crmFieldMappingAttribute => (crmFieldMappingAttribute).SchemaName).FirstOrDefault();
            }

			   if (_domainObjectType != domainObject)
            {
               attrResultName = entityNameAttr.EntitySchemaName + "." + attrResultName;
            }
			   return attrResultName;
			}
			return null;
		}

		/// <summary>
		/// 	Gets the type from constant expression.
		/// </summary>
		/// <param name="c"> The ConstantExpression. </param>
		private void GetTypeFromConstantExpression(ConstantExpression c)
		{
			Contract.Assume(c != null);

			if (_domainObjectType == null && c.Type.IsGenericType && c.Type.GetGenericTypeDefinition() == typeof(System.Linq.EnumerableQuery<>))
			{
				_domainObjectType = c.Type.GetGenericArguments()[0];
				AnalyzeResult result = SetColumns(_domainObjectType);
				if (result != null)
				{
					_queryExpession.EntityName = result.EntityName;

					foreach (var field in result.FieldsList)
					{
						_queryExpession.ColumnSet.Columns.Add(field);
					}
					
				}
			}
			else if (_domainObjectType != null && _additionObjectType == null && c.Type.IsGenericType)
			{
				_additionObjectType = c.Type.GetGenericArguments()[0];
			}
		}

		private AnalyzeResult SetColumns(Type objectType)
		{
			if (objectType != null)
			{
				AttributeParser parser = new AttributeParser(objectType);
				return parser.AnalyzeType();
			}
			return null;
		}

		/// <summary>
		/// 	Method2s the filter.
		/// </summary>
		/// <param name="m"> The method call expression. </param>
		/// <exception cref="ArgumentException">Throws
		/// 	<c>ArgumentException</c>
		/// 	, if unsupported method get's used.</exception>
		private void MethodInterpreter(MethodCallExpression m)
		{
			if (m == null)
			{
				return;
			}

			MethodInfo method = m.Method;

			switch (method.Name)
			{
				case "Contains":
					if (m.Arguments.Count < 2)
					{
						Expression value = m.Arguments[0];
						var array = Expression.Convert(m.Object, m.Object.Type);
						AddContainsCondition(array, value);
					}
					else
					{
						Expression array = m.Arguments[0];
						Expression value = m.Arguments[1];
						AddContainsCondition(array, value);
					}
					FillFilters();
					break;
				case "Where":
					var newFilter = new FilterExpression { FilterOperator = LogicalOperator.And };
					FillFilters();
					break;

				case "OrderBy":
					_currentOder.OrderType = OrderType.Ascending;
					_queryExpession.Orders.Add(_currentOder);
					_currentOder = null;
					break;

				case "OrderByDescending":
					_currentOder.OrderType = OrderType.Descending;
					_queryExpession.Orders.Add(_currentOder);
					_currentOder = null;
					break;

				case "Take":
					//SetPageSize(m);
					WriteTake(m);
					
					break;

				case "Skip":
					//SetPageNumber(m);
					WriteSkip(m);
					
					break;
				case "Join":
			      FillFilters();
					LinkEntityWith(m, JoinOperator.Inner);
					break;

            case "LeftJoin":
               FillFilters();
               LinkEntityWith(m, JoinOperator.LeftOuter);
               break;

				default:
					throw new ArgumentException(string.Format("LinQ Method {0} is not supported by this DomainContext", method.Name));
			}
		}

		

		private void FillFilters()
		{
			if (_queryExpession.Criteria == null)
			{
				_queryExpession.Criteria = new FilterExpression()
														{
															Conditions = new ObservableCollection<ConditionExpression>(),
															FilterOperator = LogicalOperator.And,
															Filters = new ObservableCollection<FilterExpression>()
														};
			}
			if (_currentCondition != null)
			{
				_queryExpession.Criteria.Conditions.Add(_currentCondition);
				_currentCondition = null;
			}
			if (_filters[LogicalOperator.And].Any(x=>x != null))
			{
				
				_queryExpession.Criteria.Conditions.AddRange(_filters[LogicalOperator.And].Where(x => x != null));				
			}
			_filters[LogicalOperator.And].Clear();

			if (_filters[LogicalOperator.Or].Any(x=>x != null))
			{
				var orFilter = new FilterExpression()
				               	{
				               		FilterOperator = LogicalOperator.Or,
											Conditions = new ObservableCollection<ConditionExpression>(),
											Filters = new ObservableCollection<FilterExpression>()
				               	};
				_queryExpession.Criteria.Filters.Add(orFilter);
				foreach (ConditionExpression expression in _filters[LogicalOperator.Or].Where(x=>x!=null))
				{
					orFilter.Filters.Add(new FilterExpression() { Conditions = new ObservableCollection<ConditionExpression> { expression } });
				}				
			}
			_filters[LogicalOperator.Or].Clear();
		}

		private void LinkEntityWith(MethodCallExpression m, JoinOperator jOperator)
		{
			if (_additionObjectType == null || m.Arguments.Count != 5)
			{
				return;
			}

			var link = new LinkEntity
							{
								LinkFromEntityName = MapAttributeNameToCrmSchemaName(null, _domainObjectType),
								LinkToEntityName = MapAttributeNameToCrmSchemaName(null, _additionObjectType),
                        JoinOperator = jOperator,
								LinkCriteria = new FilterExpression()
														{
															FilterOperator = LogicalOperator.And,
															Conditions = new ObservableCollection<ConditionExpression>()
														}
							};
			if (_currentCondition != null && _currentCondition.Values.Any())
			{
				link.LinkCriteria.Conditions.Add(_currentCondition);
			}

         
		   #region check for conditions of queryExpression
         //conditions
         List<ConditionExpression> conditions = new List<ConditionExpression>();
         conditions.AddRange(_queryExpession.Criteria.Conditions.Where(x=>x.AttributeName.StartsWith(link.LinkToEntityName)));
		   foreach (ConditionExpression cond in conditions)
		   {
		      _queryExpession.Criteria.Conditions.Remove(cond);
		      cond.AttributeName = cond.AttributeName.Replace(link.LinkToEntityName + ".", "");
            link.LinkCriteria.Conditions.Add(cond);
		   }
         //filters
         //TODO
		   #endregion

			var columns = SetColumns(_additionObjectType);
			link.EntityAlias = columns.EntityName;
			link.Columns = new ColumnSet();
			link.Columns.Columns = new ObservableCollection<string>(columns.FieldsList.ToArray());
			

			var firstName = GetMemberName(((LambdaExpression)((UnaryExpression)m.Arguments[2]).Operand).Body);
			var secondName = GetMemberName(((LambdaExpression)((UnaryExpression)m.Arguments[3]).Operand).Body);

			if (firstName != "Id" || secondName != "Id")
			{
				string linkFrom = MapAttributeNameToCrmSchemaName(firstName, _domainObjectType);
				string linkTo = MapAttributeNameToCrmSchemaName(secondName, _additionObjectType).Replace(link.LinkToEntityName+".", "");
				link.LinkFromAttributeName = linkFrom;
				link.LinkToAttributeName = linkTo;
			}

			if (_queryExpession.LinkEntities == null)
			{
				_queryExpession.LinkEntities = new ObservableCollection<LinkEntity>();
			}
			_queryExpession.LinkEntities.Add(link);

			_additionObjectType = null;
			_currentCondition = null;
		}

		private void AddContainsCondition(Expression array, Expression value)
		{
			if (_currentCondition == null)
			{
				_currentCondition = new ConditionExpression();
			}
			if (((MemberExpression)value).Expression != null && ((MemberExpression)value).Expression.Type == typeof(EntityReference))
			{
				_currentCondition.AttributeName = ((MemberExpression)((MemberExpression)value).Expression).Member.Name;
			}
			else
			{
				_currentCondition.AttributeName = ((MemberExpression)value).Member.Name;
			}

		   var usedType = _domainObjectType;
         try
         {
            var additionType = ((MemberExpression)((MemberExpression)value).Expression).Expression.Type;
            if (additionType != _domainObjectType)
            {
               usedType = additionType;
            }
         }
         catch
         {            
         }            

			_currentCondition.AttributeName = MapAttributeNameToCrmSchemaName(_currentCondition.AttributeName, usedType);

			_currentCondition.Operator = ConditionOperator.In;
			_currentCondition.Values = new ObservableCollection<object>();

			if (array.Type.IsArray || array.Type.IsGenericType)
			{
				var elType = array.Type.GetElementType();
				if (elType == null)
				{
					try
					{
						var someArray = Expression.Lambda(array).Compile().DynamicInvoke();
						_currentCondition.Values.AddRange(((IList)someArray).Cast<object>());
					}
					catch
					{
					}
				}
				else
				{
					switch (elType.Name)
					{
						case "String":
							string[] stringValue = Expression.Lambda<Func<string[]>>(array).Compile().Invoke();

							_currentCondition.Values.AddRange(stringValue);
							return;

						case "Nullable`1[System.Guid]":
							_currentCondition.Values.AddRange(Expression.Lambda<Func<Guid?[]>>(array).Compile().Invoke().Cast<object>().Where(x => x != null));
							break;

						case "Guid":
							_currentCondition.Values.AddRange(Expression.Lambda<Func<Guid[]>>(array).Compile().Invoke().Cast<object>());
							break;

						case "DateTime":
							_currentCondition.Values.AddRange(Expression.Lambda<Func<DateTime[]>>(array).Compile().Invoke().Cast<object>());
							break;

						case "Int32":
							_currentCondition.Values.AddRange(Expression.Lambda<Func<int[]>>(array).Compile().Invoke().Cast<object>());
							break;

						default:
							try
							{
								var values = Expression.Lambda<Func<object[]>>(array).Compile().Invoke();
								_currentCondition.Values.AddRange(values);
							}
							catch
							{
								throw new ArgumentException(string.Format("Contains condition uses unsupported Type {0}", elType));
							}
							break;
					}
				}
			}
		}
	
		/// <summary>
		/// 	Adds a filter condition.
		/// </summary>
		/// <param name="nodeType"> Type of the node. </param>
		/// <param name="left"> The left expression. </param>
		/// <param name="right"> The right expression. </param>
		/// <exception cref="ArgumentException">Throws
		/// 	<c>ArgumentException</c>
		/// 	if unsupported NodeType or operator Type argument get's used.</exception>
		private void AddFilterCondition(ExpressionType nodeType, Expression left, Expression right)
		{
			Contract.Requires(left != null && right != null);

			if (_currentCondition == null)
			{
				_currentCondition = new ConditionExpression();
			}

			// get operator
			switch (nodeType)
			{
				case ExpressionType.Equal:
					_currentCondition.Operator = ConditionOperator.Equal;
					break;

				case ExpressionType.NotEqual:
					_currentCondition.Operator = ConditionOperator.NotEqual;
					break;

				case ExpressionType.LessThan:
					_currentCondition.Operator = ConditionOperator.LessThan;
					break;

				case ExpressionType.GreaterThan:
					_currentCondition.Operator = ConditionOperator.GreaterThan;
					break;

				case ExpressionType.GreaterThanOrEqual:
					_currentCondition.Operator = ConditionOperator.GreaterEqual;
					break;

				case ExpressionType.LessThanOrEqual:
					_currentCondition.Operator = ConditionOperator.LessEqual;
					break;

				default:
					throw new ArgumentException(string.Format("Lambda NodeType {0} is not supported by this DomainContext", nodeType));
			}

			#region left part
			_currentCondition.AttributeName = GetMemberName(left);
			if (left is MemberExpression)
			{
				if (((MemberExpression)left).Expression != null && ((MemberExpression)left).Expression.Type == typeof(EntityReference))
				{
					_currentCondition.AttributeName = MapAttributeNameToCrmSchemaName(((MemberExpression)((MemberExpression)left).Expression).Member.Name, _domainObjectType);
				}
				else
				{
					_currentCondition.AttributeName = MapAttributeNameToCrmSchemaName(_currentCondition.AttributeName, ((MemberExpression)left).Expression.Type);
				}
			}
			else
			{
				if (left is UnaryExpression && ((UnaryExpression)left).Operand is MemberExpression)
				{
					var canBeUsed = (((UnaryExpression)left).Operand as MemberExpression).Expression.Type;
					string attr = MapAttributeNameToCrmSchemaName(_currentCondition.AttributeName, canBeUsed);
					if (string.IsNullOrEmpty(attr))
					{
						Debug.Assert(false);
						_currentCondition.AttributeName = MapAttributeNameToCrmSchemaName(_currentCondition.AttributeName, _domainObjectType);
					}
					else
					{
						_currentCondition.AttributeName = attr;
					}
				}
				else
				{
					_currentCondition.AttributeName = MapAttributeNameToCrmSchemaName(_currentCondition.AttributeName, _domainObjectType);
				}
			}
			#endregion

			#region right part
			// right
			if (_currentCondition.Values == null)
			{
				_currentCondition.Values = new ObservableCollection<object>();
			}

			object objectValue = null;

			switch (right.Type.ToString())
			{
				case "System.String":
					string stringValue = Expression.Lambda<Func<string>>(right).Compile().Invoke();

					// check if string is using like Operators
					if (stringValue.StartsWith("%") || stringValue.EndsWith("%"))
					{
						_currentCondition.Operator = ConditionOperator.Like;
					}

					_currentCondition.Values.Add(stringValue);
					return;

				case "System.Nullable`1[System.Guid]":
					objectValue = Expression.Lambda<Func<Guid?>>(right).Compile().Invoke();
					break;

            case "System.Boolean":
               objectValue = Expression.Lambda<Func<bool>>(right).Compile().Invoke();
               break;

				case "System.Nullable`1[System.DateTime]":
					objectValue = Expression.Lambda<Func<DateTime?>>(right).Compile().Invoke();
					_currentCondition.Operator = ChangeOperatorForDate(nodeType);
					break;

				case "System.Guid":
					objectValue = Expression.Lambda<Func<Guid>>(right).Compile().Invoke();
					break;

				case "System.DateTime":
					objectValue = Expression.Lambda<Func<DateTime>>(right).Compile().Invoke();
					_currentCondition.Operator = ChangeOperatorForDate(nodeType);
					break;

				case "System.Int32":
					objectValue = Expression.Lambda<Func<int>>(right).Compile().Invoke();
					break;

				case "System.Double":
					objectValue = Expression.Lambda<Func<double>>(right).Compile().Invoke();
					break;

				case "System.Nullable`1[System.Double]":
					objectValue = Expression.Lambda<Func<double?>>(right).Compile().Invoke();
					break;

				default:
					try
					{
						objectValue = Expression.Lambda<Func<object>>(right).Compile().Invoke();
					}
					catch
					{
						throw new ArgumentException(string.Format("Lambda Expression on right side uses unsupported Type {0}", right.Type));
					}

					break;
			}
			#endregion

			if (objectValue == null)
			{
				switch (nodeType)
				{
					case ExpressionType.Equal:
						_currentCondition.Operator = ConditionOperator.Null;
						return;

					case ExpressionType.NotEqual:
						_currentCondition.Operator = ConditionOperator.NotNull;
						return;

					default:
						throw new ArgumentException("Lambda Expression can only evalute null as Equal or NotEqual");
				}
			}

			_currentCondition.Values.Add(objectValue);
		}

		private ConditionOperator ChangeOperatorForDate(ExpressionType @operator)
		{
			switch (@operator)
			{
				case ExpressionType.Equal:
					return ConditionOperator.On;

				case ExpressionType.NotEqual:
					return ConditionOperator.NotOn;

				case ExpressionType.LessThan:
					return ConditionOperator.LessThan;

				case ExpressionType.GreaterThan:
					return ConditionOperator.GreaterThan;

				case ExpressionType.GreaterThanOrEqual:
					return ConditionOperator.OnOrAfter;

				case ExpressionType.LessThanOrEqual:
					return ConditionOperator.OnOrBefore;
			}
			return _currentCondition.Operator;
		}

		private string GetMemberName(Expression left)
		{
			if (left.NodeType == ExpressionType.Convert)
			{
				return ((MemberExpression)((UnaryExpression)left).Operand).Member.Name;
			}
			else if (((MemberExpression)left).Expression != null && ((MemberExpression)left).Expression.Type == typeof(EntityReference))
			{
				return ((MemberExpression)((MemberExpression)left).Expression).Member.Name;
			}
			else
			{
				return ((MemberExpression)left).Member.Name;
			}
		}

		/// <summary>
		/// 	Creates the paging info.
		/// </summary>
		private void CreatePagingInfo()
		{
			if (_currentPaging == null)
			{
				_currentPaging = new PagingInfo { Count = 0, PageNumber = 0 };
			}

			_queryExpession.PageInfo = _currentPaging;
		}

		/// <summary>
		/// 	Sets the size of the page.
		/// </summary>
		/// <param name="m"> The MethdoCallExpression. </param>
		private void SetPageSize(MethodCallExpression m)
		{
			Contract.Requires(m != null);

			CreatePagingInfo();

			if (m.Arguments.Count == 2 && m.Arguments[1] != null && m.Arguments[1].Type == typeof(int))
			{
				object objectValue = ((ConstantExpression)m.Arguments[1]).Value;
				_currentPaging.Count = (int)objectValue;

				if (_currentPaging.PageNumber > 0)
				{
					_currentPaging.PageNumber = _currentPaging.PageNumber / (int)objectValue;
				}
			}
		}

		/// <summary>
		/// 	Sets the page number.
		/// </summary>
		/// <param name="m"> The MethdoCallExpression. </param>
		private void SetPageNumber(MethodCallExpression m)
		{
			Contract.Requires(m != null);

			CreatePagingInfo();

			if (m.Arguments.Count == 2 && m.Arguments[1] != null && m.Arguments[1].Type == typeof(int))
			{
				object objectValue = ((ConstantExpression)m.Arguments[1]).Value;

				if (_currentPaging.Count == 0)
				{
					_currentPaging.Count = 1;
				}

				_currentPaging.PageNumber = (int)objectValue / _currentPaging.Count;
			}
		}

		private void WriteSkip(MethodCallExpression m)
		{
			if (m.Arguments.Count == 2 && m.Arguments[1] != null && m.Arguments[1].Type == typeof(int))
			{
				object objectValue = ((ConstantExpression)m.Arguments[1]).Value;

				_skip = (int)objectValue;
				if (_skip < 0)
				{
					throw new ArgumentOutOfRangeException("Skip count should be postive number");
				}
			}
		}

		private void WriteTake(MethodCallExpression m)
		{
			if (m.Arguments.Count == 2 && m.Arguments[1] != null && m.Arguments[1].Type == typeof(int))
			{
				object objectValue = ((ConstantExpression)m.Arguments[1]).Value;

				_take = (int)objectValue;
				if (_take < 0)
				{
					throw new ArgumentOutOfRangeException("Take count should be postive number");
				}
			}
		}

		#endregion
	

		
	}
}